﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.IO;
using System.Security.Cryptography;
using System.Threading;

namespace MirrorFolder
{
    public class DirSync
    {
        #region *** Классы и типы *************************************************************************************

        public class FileCmp
        {
            public long Len;
            public DateTime ChangeDate;
            public string MD5 = "";
            public string Path;
            public string Path2; // такой же элемент в другогом каталоге
            public string LocalPath;
            public string LOCAL_PATH;
            public string Name;
            public bool IsDirectory;
            public string LocalParentDir;
            public string LOCAL_PARENT_DIR;
            public string HomePath;

            /// <summary>расширение файла (без точки)</summary>
            public string Ext
            {
                get
                {
                    string ret = System.IO.Path.GetExtension(Name);
                    if (ret != "")
                        return ret.Substring(1);
                    return "";
                }
            }

            public FileCmp(string FilePath, string HomePath, bool IsDirectory)
            {
                Path = FilePath;
                this.HomePath = HomePath;

                FileInfo fi = new FileInfo(FilePath);
                ChangeDate = IsDirectory ? fi.CreationTimeUtc : fi.LastWriteTimeUtc;
                this.IsDirectory = IsDirectory;
                if (!IsDirectory) Len = fi.Length;
                Name = fi.Name;

                LocalPath = Path.Replace(HomePath, "");
                LOCAL_PATH = LocalPath.ToUpper();
                LocalParentDir = LocalPath.Substring(0, LocalPath.Length - Name.Length);
                LOCAL_PARENT_DIR = LocalParentDir.ToUpper();
            }

            public FileCmp(BinaryReader Br)
            {
                Br.ReadByte();
                Len = Br.ReadInt64();
                ChangeDate = new DateTime(Br.ReadInt64());
                MD5 = Br.ReadString();
                Path = Br.ReadString();
                HomePath = Br.ReadString();
                Name = Br.ReadString();
                IsDirectory = Br.ReadBoolean();

                LocalPath = Path.Replace(HomePath, "");
                LOCAL_PATH = LocalPath.ToUpper();
                LocalParentDir = LocalPath.Substring(0, LocalPath.Length - Name.Length);
                LOCAL_PARENT_DIR = LocalParentDir.ToUpper();
            }

            /// <summary>Локальные пути совпадают</summary>
            public bool LocalPathEqual(FileCmp other, bool MatchCase) { return MatchCase ? LocalPath == other.LocalPath : LOCAL_PATH == other.LOCAL_PATH; }

            public void Save(BinaryWriter Bw)
            {
                Bw.Write((byte)0);
                Bw.Write(Len);
                Bw.Write(ChangeDate.Ticks);
                Bw.Write(MD5);
                Bw.Write(Path);
                Bw.Write(HomePath);
                Bw.Write(Name);
                Bw.Write(IsDirectory);
            }

            public override string ToString()
            {
                return LocalPath;
            }

        }

        public enum ActionType { DeleteInDir1, DeleteInDir2, AddToDir1, AddToDir2, ReplaceToDir1, ReplaceToDir2, RenameToDir1, RenameToDir2 }

        public class SyncAction
        {
            /// <summary> локальный путь к элементу</summary>
            public readonly string LocalPath;

            public string LocalPathNew = "";

            /// <summary>Домашний каталог 1</summary>
            public readonly string HomeDir1;

            /// <summary>Домашний каталог 2</summary>
            public readonly string HomeDir2;

            /// <summary>Путь к элементу в каталоге 1</summary>
            string ElementPath_InDir1 { get { return HomeDir1 + LocalPath; } }
            /// <summary>Путь к элементу в каталоге 2</summary>
            string ElementPath_InDir2 { get { return HomeDir2 + LocalPath; } }

            /// <summary>тип действия</summary>
            public readonly ActionType Action;
            public readonly bool IsDir = false;

            /// <summary>Конструктор </summary>
            /// <param name="LocalPath">локальный путь к элементу</param>
            /// <param name="HomeDir1">Домашний каталог 1</param>
            /// <param name="HomeDir2">Домашний каталог 2</param>
            /// <param name="Action">Действие</param>
            /// <param name="IsDir">Элемент - это каталог</param>
            public SyncAction(string LocalPath, string HomeDir1, string HomeDir2, ActionType Action, bool IsDir)
            {
                this.LocalPath = LocalPath;
                this.HomeDir1 = HomeDir1;
                this.HomeDir2 = HomeDir2;
                this.Action = Action;
                this.IsDir = IsDir;
            }

            public override string ToString()
            {
                return ToString(true);
            }

            public string ToString(bool addDirInfo)
            {
                switch (Action)
                {
                    case ActionType.AddToDir1: return string.Format("Добав{0} {1} '{2}'{3}", isExecuted ? "лен" : "ить", IsDir ? "каталог" : "файл", LocalPath, addDirInfo ? " в каталог 1" : "");
                    case ActionType.DeleteInDir1: return string.Format("Удал{0} {1} '{2}'{3}", isExecuted ? "ен" : "ить", IsDir ? "каталог" : "файл", LocalPath, addDirInfo ? " из каталога 1" : "");
                    case ActionType.ReplaceToDir1: return string.Format("Замен{0} {1} '{2}'{3}", isExecuted ? "ен" : "ить", IsDir ? "каталог" : "файл", LocalPath, addDirInfo ? " в каталоге 1" : "");
                    case ActionType.RenameToDir1: return string.Format("Переименова{0} {1} '{2}' -> '{3}'{4}", isExecuted ? "н" : "ть", IsDir ? "каталог" : "файл", LocalPath, LocalPathNew, addDirInfo ? " в каталоге 1" : "");
                    case ActionType.AddToDir2: return string.Format("Добав{0} {1} '{2}'{3}", isExecuted ? "лен" : "ить", IsDir ? "каталог" : "файл", LocalPath, addDirInfo ? " в каталог 1" : "");
                    case ActionType.DeleteInDir2: return string.Format("Удал{0} {1} '{2}'{3}", isExecuted ? "ен" : "ить", IsDir ? "каталог" : "файл", LocalPath, addDirInfo ? " из каталога 1" : "");
                    case ActionType.ReplaceToDir2: return string.Format("Замен{0} {1} '{2}'{3}", isExecuted ? "ен" : "ить", IsDir ? "каталог" : "файл", LocalPath, addDirInfo ? " в каталоге 1" : "");
                    case ActionType.RenameToDir2: return string.Format("Переименова{0} {1} '{2}' -> '{3}'{4}", isExecuted ? "н" : "ть", IsDir ? "каталог" : "файл", LocalPath, LocalPathNew, addDirInfo ? " в каталоге 2" : "");
                }
                return "";
            }

            public bool IsExecuted { get { return isExecuted; } }
            bool isExecuted = false;

            public bool Execute(out string ErrorText)
            {
                ErrorText = "";

                if (Action == ActionType.DeleteInDir1 || Action == ActionType.DeleteInDir2)
                {
                    try
                    {
                        string ElementPath_InDir = Action == ActionType.DeleteInDir1 ? ElementPath_InDir1 : ElementPath_InDir2;
                        if (IsDir)
                        {
                            string[] fl = Directory.GetFiles(ElementPath_InDir, "*", SearchOption.AllDirectories);
                            foreach (string file in fl) File.SetAttributes(file, FileAttributes.Normal);
                            Directory.Delete(ElementPath_InDir, true);
                        }
                        else
                        {
                            File.SetAttributes(ElementPath_InDir, FileAttributes.Normal);
                            File.Delete(ElementPath_InDir);
                        }
                    }
                    catch (Exception ex)
                    {
                        ErrorText = string.Format("Ошибка удаления для '{0}'\r\n\tТекст ошибки: '{1}'", LocalPath, ex.Message);
                        return false;
                    }
                    isExecuted = true;
                    return true;
                }

                if (Action == ActionType.AddToDir1 || Action == ActionType.AddToDir2)
                {
                    try
                    {
                        string ElementPath_From = Action == ActionType.AddToDir1 ? ElementPath_InDir2 : ElementPath_InDir1;
                        string ElementPath_To = Action == ActionType.AddToDir1 ? ElementPath_InDir1 : ElementPath_InDir2;
                        if (IsDir) Directory.CreateDirectory(ElementPath_To);
                        else File.Copy(ElementPath_From, ElementPath_To);
                    }
                    catch (Exception ex)
                    {
                        ErrorText = string.Format("Ошибка добавления '{0}'\r\n\tТекст ошибки: '{1}'", LocalPath, ex.Message);
                        return false;
                    }
                    isExecuted = true;
                    return true;
                }

                if (Action == ActionType.ReplaceToDir1 || Action == ActionType.ReplaceToDir2)
                {
                    try
                    {
                        string ElementPath_From = Action == ActionType.ReplaceToDir1 ? ElementPath_InDir2 : ElementPath_InDir1;
                        string ElementPath_To = Action == ActionType.ReplaceToDir1 ? ElementPath_InDir1 : ElementPath_InDir2;
                        File.SetAttributes(ElementPath_To, FileAttributes.Normal);
                        File.Copy(ElementPath_From, ElementPath_To, true);
                    }
                    catch (Exception ex)
                    {
                        ErrorText = string.Format("Ошибка замены '{0}'\r\n\tТекст ошибки: '{1}'", LocalPath, ex.Message);
                        return false;

                    }
                    isExecuted = true;
                    return true;
                }

                if (Action == ActionType.RenameToDir1 || Action == ActionType.RenameToDir2)
                {
                    try
                    {
                        string ElementPath_Old = Action == ActionType.RenameToDir1 ? ElementPath_InDir1 : ElementPath_InDir2;
                        string ElementPath_New = Action == ActionType.RenameToDir1 ? HomeDir1 + LocalPathNew : HomeDir2 + LocalPathNew;
                        string ElementPath_Temp = ElementPath_Old;
                        while (IsDir ? Directory.Exists(ElementPath_Temp) : File.Exists(ElementPath_Temp)) ElementPath_Temp += "~";

                        if (IsDir)
                        {
                            Directory.Move(ElementPath_Old, ElementPath_Temp);
                            Directory.Move(ElementPath_Temp, ElementPath_New);
                        }
                        else
                        {
                            File.SetAttributes(ElementPath_Old, FileAttributes.Normal);
                            File.Move(ElementPath_Old, ElementPath_Temp);
                            File.Move(ElementPath_Temp, ElementPath_New);
                        }
                    }
                    catch (Exception ex)
                    {
                        ErrorText = string.Format("Ошибка переименования '{0}' -> '{1}'\r\n\tТекст ошибки: '{2}'", LocalPath, LocalPathNew, ex.Message);
                        return false;

                    }
                    isExecuted = true;
                    return true;
                }

                ErrorText = string.Format("Неизвестный тип действия '{0}'", Action.ToString());
                return false;

            }

            /// <summary>Выполнение действий для архивирования. Возвращает, удачно ли выполнение</summary>
            /// <param name="DelDirs">список для заполнения удаляемых каталогов</param>
            /// <param name="DelFiles">список для заполнения удаляемых файлов</param>
            /// <param name="ErrorText">текст ошибки в случае неудачного выполнения</param>
            public bool ExecuteArch(List<string> DelDirs, List<string> DelFiles, out string ErrorText)
            {
                ErrorText = "";
                if (Action == ActionType.DeleteInDir1 || Action == ActionType.AddToDir1 || Action == ActionType.RenameToDir1 || Action == ActionType.ReplaceToDir1)
                {
                    ErrorText = string.Format("Неподдерживаемое действие '{0}'", Action.ToString());
                    return false;
                }

                if (Action == ActionType.DeleteInDir2)
                {
                    try
                    {
                        if (IsDir) DelDirs.Add(LocalPath);
                        else DelFiles.Add(LocalPath);
                    }
                    catch (Exception ex)
                    {
                        ErrorText = string.Format("Ошибка записи удаления для '{0}'\r\n\tТекст ошибки: '{1}'", LocalPath, ex.Message);
                        return false;
                    }
                    isExecuted = true;
                    return true;
                }

                string elName = Path.GetFileName(LocalPath);
                string localParentDir = LocalPath.Substring(0, LocalPath.Length - elName.Length);
                try
                {
                    // создание родительских каталогов для записи
                    if (localParentDir.Trim() != "")
                    {
                        string[] sl = localParentDir.Split('\\');
                        string dirPath = "";
                        foreach (string dir in sl)
                        {
                            if (dirPath.Length > 0) dirPath += "\\";
                            dirPath += dir;
                            string dirPathAbs = Path.Combine(HomeDir2, dirPath);
                            if (!Directory.Exists(dirPathAbs))
                                Directory.CreateDirectory(dirPathAbs);
                        }
                    }
                }
                catch (Exception ex)
                {
                    ErrorText = string.Format("Ошибка создания родительского каталога '{0}' для '{1}'\r\n\tТекст ошибки: '{2}'", localParentDir, LocalPath, ex.Message);
                    return false;
                }

                if (Action == ActionType.AddToDir2)
                {
                    try
                    {
                        if (IsDir) CreateDirectoryWithCreateDate(ElementPath_InDir1, ElementPath_InDir2);
                        else File.Copy(ElementPath_InDir1, ElementPath_InDir2);
                    }
                    catch (Exception ex)
                    {
                        ErrorText = string.Format("Ошибка добавления '{0}'\r\n\tТекст ошибки: '{1}'", LocalPath, ex.Message);
                        return false;
                    }
                    isExecuted = true;
                    return true;
                }

                if (Action == ActionType.ReplaceToDir2)
                {
                    try
                    {
                        if (IsDir) CreateDirectoryWithCreateDate(ElementPath_InDir1, ElementPath_InDir2);
                        else File.Copy(ElementPath_InDir1, ElementPath_InDir2);
                    }
                    catch (Exception ex)
                    {
                        ErrorText = string.Format("Ошибка добавления '{0}' для замены \r\n\tТекст ошибки: '{1}'", LocalPath, ex.Message);
                        return false;

                    }
                    isExecuted = true;
                    return true;
                }

                ErrorText = string.Format("Неизвестный тип действия '{0}'", Action.ToString());
                return false;
            }

            public static void CreateDirectoryWithCreateDate(string dPathSrs, string dPathDest)
            {
                for (int i = 0; i < 10; i++)
                {
                    try
                    {
                        Directory.CreateDirectory(dPathDest);
                        FileInfo fi = new FileInfo(dPathSrs);
                        Directory.SetCreationTimeUtc(dPathDest, fi.CreationTimeUtc);
                        Directory.SetCreationTime(dPathDest, fi.CreationTime);
                        break;
                    }
                    catch (Exception ex)
                    {
                        if (ex.GetType() != typeof(IOException)) throw ex;
                        System.Threading.Thread.Sleep(1000);
                    }
                }
            }
        }

        public class Snapshot
        {
            public List<FileCmp> DList;
            public List<FileCmp> FList;
            public string HomePath = "";

            public Snapshot()
            {
                DList = new List<FileCmp>();
                FList = new List<FileCmp>();
            }

            public Snapshot(BinaryReader Br)
            {
                DList = new List<FileCmp>();
                FList = new List<FileCmp>();
                Br.ReadByte();
                HomePath = Br.ReadString();
                int len = Br.ReadInt32();
                for (int i = 0; i < len; i++) DList.Add(new FileCmp(Br));
                len = Br.ReadInt32();
                for (int i = 0; i < len; i++) FList.Add(new FileCmp(Br));
            }

            public void Save(BinaryWriter Bw)
            {
                Bw.Write((byte)0);
                Bw.Write(HomePath);
                Bw.Write(DList.Count);
                foreach (FileCmp d in DList) d.Save(Bw);
                Bw.Write(FList.Count);
                foreach (FileCmp f in FList) f.Save(Bw);
            }

            /// <summary>Конструктор. Получение снимка каталога</summary>
            /// <param name="HomePath">каталог для снимка</param>
            public Snapshot(string HomePath)
            {
                DList = new List<FileCmp>();
                FList = new List<FileCmp>();

                List<FileCmp> ret = new List<FileCmp>();
                this.HomePath = HomePath;
                if (this.HomePath[this.HomePath.Length - 1] != '\\') this.HomePath += "\\";
                string[] dl = Directory.GetDirectories(this.HomePath, "*", SearchOption.AllDirectories);
                string[] fl = Directory.GetFiles(this.HomePath, "*", SearchOption.AllDirectories);
                foreach (string d in dl) DList.Add(new FileCmp(d, this.HomePath, true));
                foreach (string f in fl) FList.Add(new FileCmp(f, this.HomePath, false));
            }

        }

        class SyncDataType
        {
            public readonly string Dir1;
            public readonly string Dir2;
            public readonly bool DualSync;
            public readonly Snapshot LastSnapshot;
            public bool MatchCase;
            public SyncDataType(string Dir1, string Dir2, bool DualSync, Snapshot LastSnapshot, bool MatchCase)
            {
                this.Dir1 = Dir1;
                this.Dir2 = Dir2;
                this.DualSync = DualSync;
                this.LastSnapshot = LastSnapshot;
                this.MatchCase = MatchCase;
            }
        }

        class ArchDataType
        {
            public readonly string SrsDir;
            public readonly string ArchDir;
            public readonly byte LineSize;
            public readonly byte LinesCount;
            public readonly ArchiveMode ArchMode;

            public ArchDataType(string SrsDir, string ArchDir, ArchiveMode ArchMode, byte LineSize, byte LinesCount)
            {
                this.SrsDir = SrsDir;
                this.ArchDir = ArchDir;
                this.LineSize = LineSize;
                this.LinesCount = LinesCount;
                this.ArchMode = ArchMode;
            }
        }

        public enum ExecuteState { None, Sync, SyncEnd_Good, SyncEnd_Err, CancelSync, SyncEnd_Cancel }

        const string archivePropertiesFileName = "Properties.bin";
        const string archiveDelElementsDirName = "DeletedInfo";

        #endregion *** Классы и типы **********************************************************************************

        #region *** Статические public методы *************************************************************************

        /// <summary>Сравнение каталогов</summary>
        /// <param name="Dir1">каталог 1</param>
        /// <param name="Dir2">каталог 2</param>
        /// <param name="UseCheckByContent">сравнивать по содержимому (если нет, сравнивается по размеру и дате последнего изменения)</param>
        /// <param name="MatchCase">учитывать регистр</param>
        /// <param name="Dirs_OnlyInDir1">каталоги, существующие только в каталоге1</param>
        /// <param name="Dirs_OnlyInDir2">каталоги, существующие только в каталоге2</param>
        /// <param name="Files_OnlyInDir1">файлы, существующие только в каталоге1</param>
        /// <param name="Files_OnlyInDir2">файлы, существующие только в каталоге2</param>
        /// <param name="DiffFiles">различающиеся файлы</param>
        /// <returns></returns>
        public static bool CompareDirectories(string Dir1, string Dir2, bool UseCheckByContent, bool MatchCase, out List<FileCmp> Dirs_OnlyInDir1, out List<FileCmp> Dirs_OnlyInDir2, out List<FileCmp> Files_OnlyInDir1, out List<FileCmp> Files_OnlyInDir2, out List<FileCmp> DiffFiles)
        {
            CompareDirectories(new Snapshot(Dir1), new Snapshot(Dir2), UseCheckByContent, MatchCase, out Dirs_OnlyInDir1, out Dirs_OnlyInDir2, out  Files_OnlyInDir1, out Files_OnlyInDir2, out DiffFiles);
            return true;
        }

        /// <summary>Сравнение каталогов</summary>
        /// <param name="Sn1">снапшот каталога1</param>
        /// <param name="Sn2">снапшот каталога2</param>
        /// <param name="UseCheckByContent">сравнивать по содержимому (если нет, сравнивается по размеру и дате последнего изменения)</param>
        /// <param name="MatchCase">учитывать регистр</param>
        /// <param name="Dirs_OnlyInDir1">каталоги, существующие только в каталоге1</param>
        /// <param name="Dirs_OnlyInDir2">каталоги, существующие только в каталоге2</param>
        /// <param name="Files_OnlyInDir1">файлы, существующие только в каталоге1</param>
        /// <param name="Files_OnlyInDir2">файлы, существующие только в каталоге2</param>
        /// <param name="DiffFiles">различающиеся файлы</param>
        /// <param name="DiffFiles"></param>
        public static void CompareDirectories(Snapshot Sn1, Snapshot Sn2, bool UseCheckByContent, bool MatchCase, out List<FileCmp> Dirs_OnlyInDir1, out List<FileCmp> Dirs_OnlyInDir2, out List<FileCmp> Files_OnlyInDir1, out List<FileCmp> Files_OnlyInDir2, out List<FileCmp> DiffFiles)
        {
            Dirs_OnlyInDir1 = new List<FileCmp>();
            Dirs_OnlyInDir2 = new List<FileCmp>();
            Files_OnlyInDir1 = new List<FileCmp>();
            Files_OnlyInDir2 = new List<FileCmp>();
            DiffFiles = new List<FileCmp>();

            // *** Сравнение каталогов *********
            Dirs_OnlyInDir1 = Sn1.DList.FindAll(dir1 => !Sn2.DList.Exists(dir2 => dir1.LocalPathEqual(dir2, MatchCase)));
            Dirs_OnlyInDir2 = Sn2.DList.FindAll(dir2 => !Sn1.DList.Exists(dir1 => dir1.LocalPathEqual(dir2, MatchCase)));

            #region *** Сравнение файлов *********

            // сравнение
            foreach (FileCmp file1 in Sn1.FList)
            {
                FileCmp file2 = Sn2.FList.FirstOrDefault(f2 => f2.LocalPathEqual(file1, MatchCase));
                // если файл существует только в каталоге 1
                if (file2 == null)
                {
                    Files_OnlyInDir1.Add(file1);
                    continue;
                }

                // сравнение
                bool bDiff = file1.Len != file2.Len;
                if (!bDiff)
                {
                    if (UseCheckByContent)
                    {
                        if (file1.MD5 == "") file1.MD5 = GetMD5(file1.Path);
                        if (file2.MD5 == "") file2.MD5 = GetMD5(file2.Path);
                        bDiff = file1.MD5 != file2.MD5;
                    }
                    else
                        bDiff = file1.ChangeDate != file2.ChangeDate;
                }

                // если файлы различаются
                if (bDiff)
                {
                    file1.Path2 = file2.Path;
                    DiffFiles.Add(file1);
                }
            }

            Files_OnlyInDir2 = Sn2.FList.FindAll(file2 => !Sn1.FList.Exists(file1 => file1.LocalPathEqual(file2, MatchCase)));

            #endregion *** Сравнение файлов *********
        }

        /// <summary>Получить список действий для копирования каталога 1 в каталог 2 (односторонняя синхронизация)</summary>
        /// <param name="Dir1">каталог 1</param>
        /// <param name="Dir2">каталог 2</param>
        /// <param name="UseCheckByContent">сравнивать по содержимому (если нет, сравнивается по размеру и времени последнего измерения)</param>
        /// <param name="MatchCase">учитывать регистр</param>
        /// <returns></returns>
        public static List<SyncAction> GetActionList(string Dir1, string Dir2, bool UseCheckByContent, bool MatchCase)
        {
            try
            {
                if (Dir1[Dir1.Length - 1] != '\\') Dir1 += "\\";
                if (Dir2[Dir2.Length - 1] != '\\') Dir2 += "\\";
                Snapshot sn1 = new Snapshot(Dir1);
                Snapshot sn2 = new Snapshot(Dir2);
                return GetActionList(Dir1, Dir2, sn1, sn2, UseCheckByContent, MatchCase);
            }
            catch { }
            return null;
        }

        /// <summary>Получить список действий для копирования каталога 1 в каталог 2 (односторонняя синхронизация)</summary>
        /// <param name="Dir1">каталог 1</param>
        /// <param name="Dir2">каталог 2</param>
        /// <param name="UseCheckByContent">сравнивать по содержимому (если нет, сравнивается по размеру и времени последнего измерения)</param>
        /// <param name="MatchCase">учитывать регистр</param>
        /// <returns></returns>
        public static List<SyncAction> GetActionList(string Dir1, string Dir2, Snapshot sn1, Snapshot sn2, bool UseCheckByContent, bool MatchCase)
        {
            List<SyncAction> actionList = new List<SyncAction>();

            if (Dir1[Dir1.Length - 1] != '\\') Dir1 += "\\";
            if (Dir2[Dir2.Length - 1] != '\\') Dir2 += "\\";

            List<FileCmp> dirs_OnlyInDir1;
            List<FileCmp> dirs_OnlyInDir2;
            List<FileCmp> files_OnlyInDir1;
            List<FileCmp> files_OnlyInDir2;
            List<FileCmp> diffFiles;
            CompareDirectories(sn1, sn2, UseCheckByContent, MatchCase, out dirs_OnlyInDir1, out dirs_OnlyInDir2, out files_OnlyInDir1, out files_OnlyInDir2, out diffFiles);

            // удаление каталогов
            foreach (FileCmp dir2 in dirs_OnlyInDir2)
            {
                // проверка, нужно ли удалять (если каталог - это дочерний каталог из того, который так же нужно удалять, действия удаления не требуется)
                if (dirs_OnlyInDir2.Exists(d2 => dir2.Path != d2.Path && dir2.LOCAL_PARENT_DIR.StartsWith(d2.LOCAL_PATH))) continue;
                actionList.Add(new SyncAction(dir2.LocalPath, Dir1, Dir2, ActionType.DeleteInDir2, true));
            }

            // удаление файлов
            foreach (FileCmp file2 in files_OnlyInDir2)
            {
                // проверка, нужно ли удалять (если файл находится в каталоге который нужно удалять, действия удаления не требуется)
                if (dirs_OnlyInDir2.Exists(d2 => file2.LOCAL_PARENT_DIR.StartsWith(d2.LOCAL_PATH))) continue;
                actionList.Add(new SyncAction(file2.LocalPath, Dir1, Dir2, ActionType.DeleteInDir2, false));
            }

            //создание каталогов
            foreach (FileCmp dir1 in dirs_OnlyInDir1) actionList.Add(new SyncAction(dir1.LocalPath, Dir1, Dir2, ActionType.AddToDir2, true));
            //добавление файлов
            foreach (FileCmp file1 in files_OnlyInDir1) actionList.Add(new SyncAction(file1.LocalPath, Dir1, Dir2, ActionType.AddToDir2, false));
            //замена файлов
            foreach (FileCmp file in diffFiles) actionList.Add(new SyncAction(file.LocalPath, Dir1, Dir2, ActionType.ReplaceToDir2, false));

            return actionList;
        }

        /// <summary>Получить список действий для двусторонней синхронизации каталогов</summary>
        /// <param name="Dir1">каталог 1</param>
        /// <param name="Dir2">каталог 2</param>
        /// <param name="Sn">Снапшот предыдущего состояния</param>
        /// <param name="UseCheckByContent">сравнивать ко содержимому (если нет, сравнивается по размеру и времени последнего измерения)</param>
        /// <param name="MatchCase">учитывать регистр</param>
        /// <returns></returns>
        public static List<SyncAction> GetActionList(string Dir1, string Dir2, Snapshot Sn, bool UseCheckByContent, bool MatchCase)
        {
            List<SyncAction> actionList = new List<SyncAction>();

            Snapshot sn1 = new Snapshot(Dir1);
            Snapshot sn2 = new Snapshot(Dir2);

            List<FileCmp> newDirs1;
            List<FileCmp> newFiles1;
            List<FileCmp> delDirs1;
            List<FileCmp> delFiles1;
            List<FileCmp> diffFiles1;

            List<FileCmp> newDirs2;
            List<FileCmp> newFiles2;
            List<FileCmp> delDirs2;
            List<FileCmp> delFiles2;
            List<FileCmp> diffFiles2;

            CompareDirectories(sn1, Sn, UseCheckByContent, false, out newDirs1, out delDirs1, out newFiles1, out delFiles1, out diffFiles1);
            CompareDirectories(sn2, Sn, UseCheckByContent, false, out newDirs2, out delDirs2, out newFiles2, out delFiles2, out diffFiles2);
            newFiles1.AddRange(diffFiles1);
            newFiles2.AddRange(diffFiles2);

            #region *** удаление каталогов ******************
            // Удаляются только те каталоги, которые:
            // - удалены в другом домашнем каталоге
            // - не удалены в текущем домашнем каталоге
            List<FileCmp> delDirList1 = delDirs2.FindAll(delDir2 => sn1.DList.Exists(dir1 => dir1.LocalPathEqual(delDir2, false)));
            List<FileCmp> delDirList2 = delDirs1.FindAll(delDir1 => sn2.DList.Exists(dir2 => dir2.LocalPathEqual(delDir1, false)));

            // удаление каталогов в dir1
            foreach (FileCmp del1 in delDirList1)
            {
                // проверка, нужно ли удалять (если каталог - это дочерний каталог из того, который так же нужно удалять, действия удаления не требуется)
                if (delDirList1.Exists(d1 => del1.Path != d1.Path && del1.LOCAL_PARENT_DIR.StartsWith(d1.LOCAL_PATH))) continue;
                actionList.Add(new SyncAction(del1.LocalPath, sn1.HomePath, sn2.HomePath, ActionType.DeleteInDir1, true));
            }
            // удаление каталогов в dir2
            foreach (FileCmp del2 in delDirList2)
            {
                // проверка, нужно ли удалять (если каталог - это дочерний каталог из того, который так же нужно удалять, действия удаления не требуется)
                if (delDirList2.Exists(d2 => del2.Path != d2.Path && del2.LOCAL_PARENT_DIR.StartsWith(d2.LOCAL_PATH))) continue;
                actionList.Add(new SyncAction(del2.LocalPath, sn1.HomePath, sn2.HomePath, ActionType.DeleteInDir2, true));
            }
            #endregion *** удаление каталогов ***************

            #region *** удаление файлов *********************
            // Удаляются только те файлы, которые:
            // - удалены в другом домашнем каталоге
            // - не удалены в текущем домашнем каталоге
            // - не изменены в текущем домашнем каталоге
            List<FileCmp> delFileList1 = delFiles2.FindAll(delFile2 => sn1.FList.Exists(file1 => file1.LocalPathEqual(delFile2, false)) && !diffFiles1.Exists(diffFile1 => diffFile1.LocalPathEqual(delFile2, false)));
            List<FileCmp> delFileList2 = delFiles1.FindAll(delFile1 => sn2.FList.Exists(file2 => file2.LocalPathEqual(delFile1, false)) && !diffFiles2.Exists(diffFile2 => diffFile2.LocalPathEqual(delFile1, false)));

            // удаление файлов в dir1
            foreach (FileCmp delFile1 in delFileList1)
            {
                // проверка, нужно ли удалять (если каталог - это дочерний каталог из того, который так же нужно удалять, действия удаления не требуется)
                if (delDirList1.Exists(d1 => delFile1.LOCAL_PARENT_DIR.StartsWith(d1.LOCAL_PATH))) continue;
                actionList.Add(new SyncAction(delFile1.LocalPath, sn1.HomePath, sn2.HomePath, ActionType.DeleteInDir1, false));
            }
            // удаление файлов в dir2
            foreach (FileCmp delFile2 in delFileList2)
            {
                // проверка, нужно ли удалять (если каталог - это дочерний каталог из того, который так же нужно удалять, действия удаления не требуется)
                if (delDirList2.Exists(d2 => delFile2.LOCAL_PARENT_DIR.StartsWith(d2.LOCAL_PATH))) continue;
                actionList.Add(new SyncAction(delFile2.LocalPath, sn1.HomePath, sn2.HomePath, ActionType.DeleteInDir2, false));
            }
            #endregion *** удаление файлов ******************

            #region *** создание каталогов ******************
            // каталог создается только если:
            // - родительский каталог не удаляется, а если удаляется, то добавляется вновь
            // - родительский каталог, где должно быть создание существует
            // - такого каталога еще нет

            // создание в каталогов, которых еще нет
            List<FileCmp> CreateInDir1List = newDirs2.FindAll(nDir2 => !newDirs1.Exists(nDir1 => nDir1.LocalPathEqual(nDir2, false)));
            List<FileCmp> CreateInDir2List = newDirs1.FindAll(nDir1 => !newDirs2.Exists(nDir2 => nDir1.LocalPathEqual(nDir2, false)));
            // проверка, нужно ли создавать
            CreateInDir1List = CreateInDir1List.FindAll(nDir => !ParentDirIsDeleted(nDir, delDirList1, delDirList2, CreateInDir1List, CreateInDir2List));
            CreateInDir2List = CreateInDir2List.FindAll(nDir => !ParentDirIsDeleted(nDir, delDirList1, delDirList2, CreateInDir1List, CreateInDir2List));
            // создание каталогов
            foreach (FileCmp cDir1 in CreateInDir1List) actionList.Add(new SyncAction(cDir1.LocalPath, sn1.HomePath, sn2.HomePath, ActionType.AddToDir1, true));
            foreach (FileCmp cDir2 in CreateInDir2List) actionList.Add(new SyncAction(cDir2.LocalPath, sn1.HomePath, sn2.HomePath, ActionType.AddToDir2, true));

            #endregion *** создание каталогов ***************

            #region *** копирование новых файлов ************
            // Файл копируется, если:
            // - каталог, в котором этот файл не удаляется, а если удаляется, то добавляется вновь
            // - каталог, куда копируется файл существует
            // - нового файла в другом каталоге нет или его дата последнего изменения меньше (downgrade не получится только в том случае, если файл был изменен сразу в обоих каталогах)

            List<FileCmp> AddInDir1List = new List<FileCmp>();
            List<FileCmp> ReplaceInDir1List = new List<FileCmp>();
            List<FileCmp> AddInDir2List = new List<FileCmp>();
            List<FileCmp> ReplaceInDir2List = new List<FileCmp>();

            // заполнение списка для добавления в первый каталог и для замены в первый или второй каталог
            foreach (FileCmp nFile2 in newFiles2)
            {
                FileCmp nFile1 = newFiles1.FirstOrDefault(nF1 => nF1.LocalPathEqual(nFile2, false)); // уже существующий новый файл
                bool fileExist = sn1.FList.Exists(f1 => f1.LocalPathEqual(nFile2, false)); // файл уже существует (нужен replace, а не add)
                if (nFile1 == null)
                {
                    if (ParentDirIsDeleted(nFile2, delDirList1, delDirList2, CreateInDir1List, CreateInDir2List)) continue;
                    if (fileExist) ReplaceInDir1List.Add(nFile2);
                    else AddInDir1List.Add(nFile2);
                }
                else
                {
                    if (!fileExist) throw new Exception("Ошибка алгоритма. Есть новый файл, но его нет в каталоге");
                    if (nFile2.ChangeDate > nFile1.ChangeDate)
                    {
                        if (ParentDirIsDeleted(nFile2, delDirList1, delDirList2, CreateInDir1List, CreateInDir2List)) continue;
                        ReplaceInDir1List.Add(nFile2);
                    }
                    else
                    {
                        if (ParentDirIsDeleted(nFile1, delDirList1, delDirList2, CreateInDir1List, CreateInDir2List)) continue;
                        ReplaceInDir2List.Add(nFile1);
                    }
                }
            }

            // заполнение списка для добавления и замены во второй каталог
            foreach (FileCmp nFile1 in newFiles1)
            {
                if (ParentDirIsDeleted(nFile1, delDirList1, delDirList2, CreateInDir1List, CreateInDir2List)) continue;

                FileCmp nFile2 = newFiles2.FirstOrDefault(nF2 => nF2.LocalPathEqual(nFile1, false)); // уже существующий новый файл
                bool fileExist = sn2.FList.Exists(f2 => f2.LocalPathEqual(nFile1, false)); // файл уже существует (нужен replace, а не add)
                if (nFile2 == null)
                {
                    if (fileExist) ReplaceInDir2List.Add(nFile1);
                    else AddInDir2List.Add(nFile1);
                }
            }

            // добавление и замена
            foreach (FileCmp nFile2 in AddInDir1List) actionList.Add(new SyncAction(nFile2.LocalPath, sn1.HomePath, sn2.HomePath, ActionType.AddToDir1, false));
            foreach (FileCmp nFile2 in ReplaceInDir1List) actionList.Add(new SyncAction(nFile2.LocalPath, sn1.HomePath, sn2.HomePath, ActionType.ReplaceToDir1, false));
            foreach (FileCmp nFile1 in AddInDir2List) actionList.Add(new SyncAction(nFile1.LocalPath, sn1.HomePath, sn2.HomePath, ActionType.AddToDir2, false));
            foreach (FileCmp nFile1 in ReplaceInDir2List) actionList.Add(new SyncAction(nFile1.LocalPath, sn1.HomePath, sn2.HomePath, ActionType.ReplaceToDir2, false));

            #endregion *** копирование новых файлов *********

            #region *** переименование **********************
            if (MatchCase)
            {
                // нахождение элементов, существующих в обоих каталогах для домашнего каталога 1
                List<FileCmp> crossList1 = sn1.DList.FindAll(el1 => sn2.DList.Exists(el2 => el1.LocalPathEqual(el2, false)));
                crossList1.AddRange(sn1.FList.FindAll(el1 => sn2.FList.Exists(el2 => el1.LocalPathEqual(el2, false))));
                // нахождение элементов, существующих в обоих каталогах для домашнего каталога 1
                List<FileCmp> crossList2 = sn2.DList.FindAll(el2 => sn1.DList.Exists(el1 => el1.LocalPathEqual(el2, false)));
                crossList2.AddRange(sn2.FList.FindAll(el2 => sn1.FList.Exists(el1 => el1.LocalPathEqual(el2, false))));

                List<FileCmp> snElList = new List<FileCmp>(Sn.DList);
                snElList.AddRange(Sn.FList);

                // нахождение элементов, c измененным ргистром в каталоге1. 
                // Это все кросс-элементы, (которых нет в снапшоте или которые отличаются по регистру в снапшоте) И которые отличаются по регистру в каталоге2
                List<FileCmp> renameInDir2 = crossList1.FindAll(el1 =>
                    {
                        FileCmp snEl = snElList.FirstOrDefault(el => el1.LocalPathEqual(el, false));
                        bool ret = snEl == null || snEl.Name != el1.Name;
                        if (!ret) return false;
                        return crossList2.First(el2 => el1.LocalPathEqual(el2, false)).Name != el1.Name;

                    });
                // нахождение элементов, c измененным ргистром в каталоге2. 
                // Это все кросс-элементы, (которых нет в снапшоте или которые отличаются по регистру в снапшоте) И которые еще не найдены для переименования в другом каталоге И которые отличаются по регистру в каталоге1
                List<FileCmp> renameInDir1 = crossList2.FindAll(el2 =>
                    {
                        if (renameInDir2.Exists(el1 => el2.LocalPathEqual(el1, false))) return false;
                        FileCmp snEl = snElList.FirstOrDefault(el => el2.LocalPathEqual(el, false));
                        bool ret = snEl == null || snEl.Name != el2.Name;
                        if (!ret) return false;
                        return crossList1.First(el1 => el2.LocalPathEqual(el1, false)).Name != el2.Name;
                    });

                foreach (FileCmp rEl2 in renameInDir2)
                {
                    string localPath = crossList2.Find(el2 => rEl2.LocalPathEqual(el2, false)).LocalPath;
                    SyncAction action = new SyncAction(localPath, sn1.HomePath, sn2.HomePath, ActionType.RenameToDir2, rEl2.IsDirectory);
                    action.LocalPathNew = rEl2.LocalPath;
                    actionList.Add(action);
                }
                foreach (FileCmp rEl1 in renameInDir1)
                {
                    string localPath = crossList1.Find(el1 => rEl1.LocalPathEqual(el1, false)).LocalPath;
                    SyncAction action = new SyncAction(localPath, sn1.HomePath, sn2.HomePath, ActionType.RenameToDir1, rEl1.IsDirectory);
                    action.LocalPathNew = rEl1.LocalPath;
                    actionList.Add(action);
                }
            }
            #endregion *** переименование *******************

            return actionList;
        }

        /// <summary>Родительский каталог элемента удаляется и не добавляется вновь</summary>
        static bool ParentDirIsDeleted(FileCmp El, List<FileCmp> DelDirList1, List<FileCmp> DelDirList2, List<FileCmp> AddDirList1, List<FileCmp> AddDirList2)
        {
            bool bDelete = DelDirList1.Exists(d1 => El.LOCAL_PARENT_DIR.StartsWith(d1.LOCAL_PATH)) || DelDirList2.Exists(d2 => El.LOCAL_PARENT_DIR.StartsWith(d2.LOCAL_PATH));
            bool bAdd = AddDirList1.Exists(d1 => El.LOCAL_PARENT_DIR == (d1.LOCAL_PATH + "\\")) || AddDirList2.Exists(d2 => El.LOCAL_PARENT_DIR == (d2.LOCAL_PATH + "\\"));
            return bDelete && !bAdd;
        }

        /// <summary>Сраснение каталогов. Возвращает, идентичны ли каталоги</summary>
        public static bool DirsIsEquals(string Dir1, string Dir2, bool UseCheckByContent, bool MatchCase)
        {
            List<FileCmp> Dirs_OnlyInDir1 = new List<FileCmp>();
            List<FileCmp> Dirs_OnlyInDir2 = new List<FileCmp>();
            List<FileCmp> Files_OnlyInDir1 = new List<FileCmp>();
            List<FileCmp> Files_OnlyInDir2 = new List<FileCmp>();
            List<FileCmp> DiffFiles = new List<FileCmp>();

            CompareDirectories(Dir1, Dir2, UseCheckByContent, MatchCase, out Dirs_OnlyInDir1, out  Dirs_OnlyInDir2, out Files_OnlyInDir1, out Files_OnlyInDir2, out DiffFiles);
            return Dirs_OnlyInDir1.Count == 0 && Dirs_OnlyInDir2.Count == 0 && Files_OnlyInDir1.Count == 0 && Files_OnlyInDir2.Count == 0 && DiffFiles.Count == 0;
        }

        /// <summary>Сраснение каталогов. Возвращает, идентичны ли каталоги</summary>
        public static bool SnapshotsIsEquals(Snapshot sn1, Snapshot sn2, bool UseCheckByContent, bool MatchCase)
        {
            List<FileCmp> Dirs_OnlyInDir1 = new List<FileCmp>();
            List<FileCmp> Dirs_OnlyInDir2 = new List<FileCmp>();
            List<FileCmp> Files_OnlyInDir1 = new List<FileCmp>();
            List<FileCmp> Files_OnlyInDir2 = new List<FileCmp>();
            List<FileCmp> DiffFiles = new List<FileCmp>();
            CompareDirectories(sn1, sn2, UseCheckByContent, MatchCase, out Dirs_OnlyInDir1, out  Dirs_OnlyInDir2, out Files_OnlyInDir1, out Files_OnlyInDir2, out DiffFiles);
            return Dirs_OnlyInDir1.Count == 0 && Dirs_OnlyInDir2.Count == 0 && Files_OnlyInDir1.Count == 0 && Files_OnlyInDir2.Count == 0 && DiffFiles.Count == 0;
        }

        /// <summary>Получение снапшота для дифференциального архива</summary>
        /// <param name="DirFull">каталог полного архива</param>
        /// <param name="DirDiff">каталог следующего подархива</param>
        public static Snapshot GetArchSnapshot(string DirFull, string DirDiff, List<string> delDirs, List<string> delFiles)
        {
            return GetArchSnapshot(new Snapshot(DirFull), new Snapshot(DirDiff), delDirs, delFiles);
        }

        /// <summary>Получение снапшота для дифференциального архива</summary>
        /// <param name="snFull">снапшот полного архива</param>
        /// <param name="snDiff">снапшот следующего подархива</param>
        public static Snapshot GetArchSnapshot(Snapshot snFull, Snapshot snDiff, List<string> delDirs, List<string> delFiles)
        {
            Snapshot ret = snFull;
            // удаление каталогов
            ret.DList = ret.DList.FindAll(dir => !delDirs.Exists(delDir => delDir == dir.LocalPath || dir.LocalParentDir.StartsWith(delDir)));
            // удаление файлов
            ret.FList = ret.FList.FindAll(file => !delFiles.Exists(delFile => delFile == file.LocalPath) && !delDirs.Exists(delDir => file.LocalParentDir.StartsWith(delDir)));

            // добавление новых каталогов
            List<FileCmp> addDirL = snDiff.DList.FindAll(dirDiff => !snFull.DList.Exists(dirFull => dirDiff.LocalPath == dirFull.LocalPath));
            ret.DList.AddRange(addDirL);
            // сортировка каталогов
            ret.DList.Sort((dir1, dir2) => dir1.LocalPath.Count(ch => ch == '\\').CompareTo(dir2.LocalPath.Count(ch => ch == '\\')));

            // добавление новых файлов
            List<FileCmp> addFileL = snDiff.FList.FindAll(fileDiff => !snFull.FList.Exists(fileFull => fileDiff.LocalPath == fileFull.LocalPath));
            ret.FList.AddRange(addFileL);

            // замена файлов
            List<FileCmp> changeFileL = snDiff.FList.FindAll(fileDiff => !addFileL.Exists(fileAdd => fileAdd.LocalPath == fileDiff.LocalPath));
            ret.FList = ret.FList.FindAll(file => !changeFileL.Exists(chFile => chFile.LocalPath == file.LocalPath));
            ret.FList.AddRange(changeFileL);

            return snFull;
        }

        #endregion *** Статические public методы **********************************************************************

        public int Percent = 0;
        object objLock = new object();
        public ExecuteState State = ExecuteState.None;
        public string ErrorText = "";
        public List<SyncAction> ActionList = new List<SyncAction>();
        public Snapshot NewSnapshot = null;
        public DateTime LastArchDate = DateTime.MinValue;
        public bool CreatedFullCopy = false;

        #region *** Синхронизация *************************************************************************************

        public void StartSyncDirectories(string Dir1, string Dir2, bool DualSync, Snapshot LastSnapshot, bool MatchCase)
        {
            ErrorText = "";
            Percent = 0;
            ActionList.Clear();
            NewSnapshot = null;
            if (!Directory.Exists(Dir1))
            {
                ErrorText = string.Format("Каталог '{0}' не существует или не доступен", Dir1);
                State = ExecuteState.SyncEnd_Err;
                return;
            }
            if (!Directory.Exists(Dir2))
            {
                ErrorText = string.Format("Каталог '{0}' не существует или не доступен", Dir2);
                State = ExecuteState.SyncEnd_Err;
                return;
            }
            Thread th = new Thread(new ParameterizedThreadStart(SyncDirectoriesInThread));
            th.IsBackground = true;
            State = ExecuteState.Sync;
            th.Start(new SyncDataType(Dir1, Dir2, DualSync, LastSnapshot, MatchCase));
        }

        void SyncDirectoriesInThread(object SyncData)
        {
            lock (objLock) Percent = 1;

            SyncDataType syncData = (SyncDataType)SyncData;
            List<SyncAction> actionList = null;
            try
            {
                if (syncData.DualSync && syncData.LastSnapshot != null)
                    actionList = GetActionList(syncData.Dir1, syncData.Dir2, syncData.LastSnapshot, false, syncData.MatchCase);
                else
                    actionList = GetActionList(syncData.Dir1, syncData.Dir2, false, syncData.MatchCase);
            }
            catch (Exception ex)
            {
                lock (objLock)
                {
                    ErrorText = string.Format("Ошибка получения списка действий\r\n\tТекст ошибки: '{0}'", ex.Message);
                    State = ExecuteState.SyncEnd_Err;
                }
                return;
            }

            lock (objLock)
            {
                Percent = 10;
                ActionList = actionList;
                if (State == ExecuteState.CancelSync)
                {
                    State = ExecuteState.SyncEnd_Cancel;
                    return;
                }
            }

            for (int i = 0; i < actionList.Count; i++)
            {
                string err;
                if (!actionList[i].Execute(out err))
                {
                    lock (objLock)
                    {
                        ErrorText = err;
                        State = ExecuteState.SyncEnd_Err;
                    }
                    return;
                }
                lock (objLock)
                {
                    Percent = (int)(10 + (90 / (double)actionList.Count) * i);
                    //if (Percent < 0) Percent = 10;
                    //if (Percent > 100) Percent = 100;
                    if (State == ExecuteState.CancelSync)
                    {
                        State = ExecuteState.SyncEnd_Cancel;
                        return;
                    }
                }
            }

            if (syncData.DualSync)
            {
                try
                {
                    Snapshot sn = new Snapshot(syncData.Dir1);
                    lock (objLock) NewSnapshot = sn;
                }
                catch (Exception ex)
                {
                    lock (objLock)
                    {
                        ErrorText = "Ошибка получения нового снапшота: " + ex.Message;
                        State = ExecuteState.SyncEnd_Err;
                    }
                    return;
                }
            }

            // контрольная проверка
            if (!DirsIsEquals(syncData.Dir1, syncData.Dir2, false, syncData.MatchCase))
            {
                lock (objLock)
                {
                    ErrorText = "Ошибка алгоритма синхронизации!";
                    State = ExecuteState.SyncEnd_Err;
                }
                return;
            }

            lock (objLock) State = ExecuteState.SyncEnd_Good;
        }

        #endregion *** Синхронизация **********************************************************************************

        #region *** Архивация *****************************************************************************************

        public void StartArchiveDirectory(string SrsDir, string DestDir, ArchiveMode ArchMode, byte LineSize, byte LinesCount)
        {
            ErrorText = "";
            Percent = 0;
            ActionList.Clear();
            NewSnapshot = null;
            LastArchDate = DateTime.MinValue;
            CreatedFullCopy = false;
            if (!Directory.Exists(SrsDir))
            {
                ErrorText = string.Format("Исходный каталог '{0}' не существует или не доступен", SrsDir);
                State = ExecuteState.SyncEnd_Err;
                return;
            }
            if (!Directory.Exists(DestDir))
            {
                ErrorText = string.Format("Каталог архива '{0}' не существует или не доступен", DestDir);
                State = ExecuteState.SyncEnd_Err;
                return;
            }
            Thread th = new Thread(new ParameterizedThreadStart(ArchiveDirectoryInThread));
            th.IsBackground = true;
            State = ExecuteState.Sync;
            th.Start(new ArchDataType(SrsDir, DestDir, ArchMode, LineSize, LinesCount));
        }

        public void ArchiveDirectoryInThread(object ArchData)
        {
            lock (objLock) Percent = 1;
            try
            {
                ArchDataType archData = (ArchDataType)ArchData;

                #region *** работа с линиями *********

                List<ArchiveLine> aLines = GetArchiveLines(archData.ArchDir, true);

                // удаление невалидных линий (изначально созданных невалидно)
                for (int i = 0; i < aLines.Count; i++)
                {
                    if (aLines[i].NotValid)
                    {
                        DeleteDir(Path.Combine(archData.ArchDir, aLines[i].DirName));
                        aLines.RemoveAt(i);
                        i--;
                    }
                }

                // определение текущей линии
                ArchiveLine actLine = null;
                string actLineDirPath = "";
                bool bFullCopy = true;
                // если линий нет, создание первой
                if (aLines.Count == 0)
                {
                    string lineName = GetNextArchLineName(archData.ArchDir);
                    actLineDirPath = Path.Combine(archData.ArchDir, lineName);
                    Directory.CreateDirectory(actLineDirPath);
                    Directory.CreateDirectory(Path.Combine(actLineDirPath, archiveDelElementsDirName));
                    actLine = new ArchiveLine(lineName, DateTime.Now, archData.ArchMode);
                    aLines.Add(actLine);
                    bFullCopy = true;
                }
                else
                {
                    actLineDirPath = Path.Combine(archData.ArchDir, aLines[aLines.Count - 1].DirName);
                    actLine = aLines[aLines.Count - 1];
                    bFullCopy = false;

                    // удаление всех невалидных архивов в линии
                    while (actLine.ArchL.Count > 1 && actLine.ArchL[actLine.ArchL.Count - 1].DirName.ToUpper() != actLine.LastGoodArchDirName.ToUpper())
                    {
                        DeleteDir(Path.Combine(Path.Combine(archData.ArchDir, actLine.DirName), actLine.ArchL[actLine.ArchL.Count - 1].DirName));
                        actLine.ArchL.RemoveAt(actLine.ArchL.Count - 1);
                    }
                }

                // если тип архива отлчается от требуемого или линия заполнена, создание новой линии
                if (archData.ArchMode != actLine.ArchMode || (archData.LineSize > 0 && actLine.ArchL.Count >= archData.LineSize))
                {
                    string lineName = GetNextArchLineName(archData.ArchDir);
                    actLineDirPath = Path.Combine(archData.ArchDir, lineName);
                    Directory.CreateDirectory(actLineDirPath);
                    Directory.CreateDirectory(Path.Combine(actLineDirPath, archiveDelElementsDirName));
                    actLine = new ArchiveLine(lineName, DateTime.Now, archData.ArchMode);
                    aLines.Add(actLine);
                    bFullCopy = true;
                }

                // удаление старых линий
                if (archData.LinesCount > 0)
                {
                    while (aLines.Count > archData.LinesCount)
                    {
                        DeleteDir(Path.Combine(archData.ArchDir, aLines[0].DirName));
                        aLines.RemoveAt(0);
                    }
                }

                lock (objLock)
                {
                    Percent = 5;
                    if (State == ExecuteState.CancelSync) { State = ExecuteState.SyncEnd_Cancel; return; }
                }

                #endregion *** работа с линиями *********

                string archDirName = "";
                // если создание полной копии
                if (bFullCopy)
                {
                    try
                    {
                        CreatedFullCopy = true;
                        archDirName = "FULL";
                        string[] dl = Directory.GetDirectories(archData.SrsDir, "*", SearchOption.AllDirectories);
                        string[] fl = Directory.GetFiles(archData.SrsDir, "*", SearchOption.AllDirectories);
                        string archDirPath = Path.Combine(actLineDirPath, archDirName);
                        lock (objLock)
                        {
                            Percent = 10;
                            if (State == ExecuteState.CancelSync) { State = ExecuteState.SyncEnd_Cancel; return; }
                        }
                        Directory.CreateDirectory(archDirPath);
                        string homePath = archData.SrsDir.Trim(); if (homePath[homePath.Length - 1] != '\\') homePath += "\\";
                        foreach (string dir in dl)
                        {
                            SyncAction.CreateDirectoryWithCreateDate(dir, Path.Combine(archDirPath, dir.Replace(homePath, "")));
                        }
                        lock (objLock)
                        {
                            Percent = 20;
                            if (State == ExecuteState.CancelSync) { State = ExecuteState.SyncEnd_Cancel; return; }
                        }
                        for (int i = 0; i < fl.Length; i++)
                        {
                            string destFilePath = Path.Combine(archDirPath, fl[i].Replace(homePath, ""));
                            try
                            {
                                File.Copy(fl[i], destFilePath);
                            }
                            catch (Exception ex)
                            {
                                string parentDir = destFilePath.Substring(0, destFilePath.Length - destFilePath.LastIndexOf('\\'));
                                AddErrInTh(string.Format("Ошибка копирования файла '{0}' в каталог '{1}'", fl[i], parentDir), ex); return;
                            }
                            lock (objLock)
                            {
                                Percent = 20 + (int)((79.0 / (double)fl.Length) * i);
                                if (State == ExecuteState.CancelSync) { State = ExecuteState.SyncEnd_Cancel; return; }
                            }
                        }
                    }
                    catch (Exception ex) { AddErrInTh("Ошибка копирования файлов", ex); return; }
                }
                // если создание диф или инк копии
                else
                {
                    List<SyncAction> actionList = null;
                    string lineDir = Path.Combine(archData.ArchDir, actLine.DirName);
                    archDirName = GetNextArchElName(lineDir, archData.ArchMode);
                    string archDirPath = Path.Combine(lineDir, archDirName);
                    Snapshot snSrs = new Snapshot(archData.SrsDir);
                    // получение списка действий
                    if (archData.ArchMode == ArchiveMode.Diff)
                    {
                        lock (objLock) LastArchDate = actLine.CreateTime;
                        Snapshot fullArchSnapshot = new Snapshot(Path.Combine(lineDir, "FULL"));
                        actionList = GetActionList(archData.SrsDir, archDirPath, snSrs, fullArchSnapshot, false, true);
                    }
                    else
                    {
                        lock (objLock) LastArchDate = actLine.ArchL[actLine.ArchL.Count - 1].CreateTime;
                        Snapshot lastArchSnapshot = new Snapshot(Path.Combine(lineDir, "FULL"));
                        for (int i = 1; i < actLine.ArchL.Count; i++)
                        {
                            List<string> delD, delF;
                            ArchiveEl.ReadDeletedInfo(lineDir, actLine.ArchL[i].DirName, out delD, out delF);
                            lastArchSnapshot = GetArchSnapshot(lastArchSnapshot, new Snapshot(Path.Combine(lineDir, actLine.ArchL[i].DirName)), delD, delF);
                        }

                        actionList = GetActionList(archData.SrsDir, archDirPath, snSrs, lastArchSnapshot, false, true);
                    }

                    lock (objLock)
                    {
                        Percent = 20;
                        if (State == ExecuteState.CancelSync) { State = ExecuteState.SyncEnd_Cancel; return; }
                        ActionList = actionList;
                    }
                    Directory.CreateDirectory(archDirPath);

                    // выполнение действий
                    List<string> delDirs = new List<string>();
                    List<string> delFiles = new List<string>();
                    for (int i = 0; i < actionList.Count; i++)
                    {
                        string err;
                        if (!actionList[i].ExecuteArch(delDirs, delFiles, out err))
                        {
                            lock (objLock) { ErrorText = err; State = ExecuteState.SyncEnd_Err; }
                            return;
                        }
                        lock (objLock)
                        {
                            Percent = (int)(20 + (79 / (double)actionList.Count) * i);
                            if (State == ExecuteState.CancelSync) { State = ExecuteState.SyncEnd_Cancel; return; }
                        }
                    }

                    // сохранение данных по удаленным каталогам и файлам
                    ArchiveEl.WriteDeletedInfo(lineDir, archDirName, delDirs, delFiles);

                    // контрольная проверка
                    Snapshot snNewArch = null;
                    if (archData.ArchMode == ArchiveMode.Diff) snNewArch = GetArchSnapshot(Path.Combine(lineDir, "FULL"), Path.Combine(lineDir, archDirPath), delDirs, delFiles);
                    else
                    {
                        snNewArch = new Snapshot(Path.Combine(lineDir, "FULL"));
                        for (int i = 1; i < actLine.ArchL.Count; i++)
                        {
                            List<string> delD, delF;
                            ArchiveEl.ReadDeletedInfo(lineDir, actLine.ArchL[i].DirName, out delD, out delF);
                            snNewArch = GetArchSnapshot(snNewArch, new Snapshot(Path.Combine(lineDir, actLine.ArchL[i].DirName)), delD, delF);
                        }
                        snNewArch = GetArchSnapshot(snNewArch, new Snapshot(Path.Combine(lineDir, Path.Combine(lineDir, archDirPath))), delDirs, delFiles);
                    }
                    if (!SnapshotsIsEquals(snSrs, snNewArch, false, true))
                    {
                        lock (objLock)
                        {
                            ErrorText = "Ошибка алгоритма архивации! Конечный архив не совпадает с исходным каталогом";
                            State = ExecuteState.SyncEnd_Err;
                        }
                        return;
                    }
                }

                // запись свойств линии
                File.WriteAllLines(Path.Combine(actLineDirPath, archPropertiesFileName), new string[] { archData.ArchMode.ToString(), archDirName }, Encoding.UTF8);
                lock (objLock) Percent = 100;
            }
            catch (Exception ex)
            {
                lock (objLock)
                {
                    ErrorText = string.Format("ОБЩАЯ ОШИБКА СОЗДАНИЯ АРХИВА\r\n\tТекст ошибки: '{0}'", ex.Message);
                    State = ExecuteState.SyncEnd_Err;
                }
                return;
            }
            lock (objLock) State = ExecuteState.SyncEnd_Good;
        }

        void AddErrInTh(string text, Exception ex)
        {
            lock (objLock)
            {
                ErrorText = string.Format("{0}\r\n\tТекст ошибки: '{1}'", text, ex.Message);
                State = ExecuteState.SyncEnd_Err;
            }
        }

        const string archPropertiesFileName = "LineProperties.prp";

        public class ArchiveLine
        {
            /// <summary>линия архива была изначально создана неверно</summary>
            public bool NotValid = false;

            public string DirName;
            public int DirNum;
            public DateTime CreateTime;
            public ArchiveMode ArchMode;
            public int MaxArchNum = -1;
            public string LastArchDirName { get { return ArchL[ArchL.Count - 1].DirName; } }
            public string LastGoodArchDirName = "";
            /// <summary>список архивов, упорядоченных по созданию (от самого раннего)</summary>
            public List<ArchiveEl> ArchL = new List<ArchiveEl>();
            public ArchiveLine(string DirName, DateTime CreateTime, ArchiveMode ArchMode)
            {
                this.DirName = DirName;
                this.DirNum = Convert.ToInt32(DirName.Substring(1));
                this.CreateTime = CreateTime;
                this.ArchMode = ArchMode;
            }
        }

        public class ArchiveEl
        {
            public string DirName;
            public DateTime CreateTime;
            public List<string> DelDirs = new List<string>();
            public List<string> DelFiles = new List<string>();
            public ArchiveEl(string DirName, DateTime CreateTime, string lineDirPath)
            {
                this.DirName = DirName;
                this.CreateTime = CreateTime;
                if (!string.IsNullOrEmpty(lineDirPath))
                    ReadDeletedInfo(lineDirPath, DirName, out DelDirs, out DelFiles);
            }

            public static void ReadDeletedInfo(string lineDirPath, string archElName, out List<string> DelDirs, out List<string> DelFiles)
            {
                string[] sl = File.ReadAllLines(Path.Combine(Path.Combine(lineDirPath, archiveDelElementsDirName), archElName + ".dLst"), Encoding.UTF8);
                try
                {
                    DelDirs = new List<string>();
                    DelFiles = new List<string>();
                    List<string> actList = null;
                    foreach (string str in sl)
                    {
                        if (str.Trim() == "<DIRS>") { actList = DelDirs; continue; }
                        if (str.Trim() == "<FILES>") { actList = DelFiles; continue; }
                        actList.Add(str);
                    }
                }
                catch { throw new Exception("Ошибка получения информации об удаленных элементах в архиве"); }
            }

            public static void WriteDeletedInfo(string lineDirPath, string archElName, List<string> delDirs, List<string> delFiles)
            {
                List<string> delList = new List<string>();
                delList.Add("<DIRS>");
                delList.AddRange(delDirs);
                delList.Add("<FILES>");
                delList.AddRange(delFiles);
                File.WriteAllLines(Path.Combine(Path.Combine(lineDirPath, archiveDelElementsDirName), archElName + ".dLst"), delList.ToArray(), Encoding.UTF8);
            }

        }

        /// <summary>Получение списка линий в архиве упорядоченные по времени создания</summary>
        public static List<ArchiveLine> GetArchiveLines(string archDirPath, bool getNotValidLines)
        {
            List<ArchiveLine> ret = new List<ArchiveLine>();

            // определение текущей линии
            string[] dl = Directory.GetDirectories(archDirPath, "L*");
            // получение последней линии
            if (dl.Length > 0)
            {
                int maxNumLine = 0;
                DateTime createDate = DateTime.MinValue;
                foreach (string dir in dl)
                {
                    // проверка корректности имени линии
                    FileInfo fi = new FileInfo(dir);
                    int n = 0;
                    if (!int.TryParse(fi.Name.Substring(1).Trim(), out n)) continue;
                    if (n < 0) continue;

                    // проверка валидности
                    bool notValid = false;
                    if (!Directory.Exists(Path.Combine(dir, "FULL"))) notValid = true;
                    if (!File.Exists(Path.Combine(dir, archPropertiesFileName))) notValid = true;
                    if (notValid)
                    {
                        if (getNotValidLines)
                        {
                            ArchiveLine nLine = new ArchiveLine(fi.Name, fi.CreationTime, ArchiveMode.Diff);
                            nLine.NotValid = true;
                            ret.Add(nLine);
                        }
                        continue;
                    }

                    ArchiveMode lineArchMode;
                    string lastGoodArchDirName = "";

                    // загрузка свойств линии
                    try
                    {
                        string[] sl = File.ReadAllLines(Path.Combine(dir, archPropertiesFileName), Encoding.UTF8);
                        lineArchMode = (ArchiveMode)Enum.Parse(typeof(ArchiveMode), sl[0].Trim());
                        lastGoodArchDirName = sl[1].Trim();
                    }
                    catch { continue; }
                    if (maxNumLine < n) maxNumLine = n;

                    ArchiveLine aLine = new ArchiveLine(fi.Name, fi.CreationTime, lineArchMode);
                    aLine.LastGoodArchDirName = lastGoodArchDirName;
                    aLine.NotValid = notValid;

                    // загрузка подархивов
                    string[] dlA = Directory.GetDirectories(dir, lineArchMode == ArchiveMode.Diff ? "D_*" : "I_*");
                    DateTime archCreateDate = DateTime.MinValue;
                    int maxArchNum = -1;
                    foreach (string dirA in dlA)
                    {
                        if (Path.GetFileName(dirA).ToUpper() == "FULL") continue;
                        FileInfo fiA = new FileInfo(dirA);
                        if (!int.TryParse(fiA.Name.Substring(2).Trim(), out n)) continue;
                        if (n < 0) continue;
                        if (maxArchNum < n) maxArchNum = n;
                        try
                        {
                            aLine.ArchL.Add(new ArchiveEl(fiA.Name, fiA.CreationTime, dir));
                        }
                        catch { continue; }
                    }
                    aLine.MaxArchNum = maxArchNum;
                    aLine.ArchL.Sort((a1, a2) => a1.CreateTime.CompareTo(a2.CreateTime));
                    aLine.ArchL.Insert(0, new ArchiveEl("FULL", aLine.CreateTime, null));

                    ret.Add(aLine);
                }
            }
            ret.Sort((l1, l2) => l1.CreateTime.CompareTo(l2.CreateTime));

            return ret;
        }

        public static string GetNextArchLineName(string archDirPath)
        {
            string[] dl = Directory.GetDirectories(archDirPath, "L*");
            int maxLineNum = -1;
            foreach (string dir in dl)
            {
                int n;
                if (!int.TryParse(Path.GetFileName(dir).Substring(1).Trim(), out n)) continue;
                if (maxLineNum < n) maxLineNum = n;
            }
            string lineName = "";
            do
            {
                maxLineNum += 1;
                lineName = "L" + (maxLineNum < 100 ? maxLineNum.ToString("00") : maxLineNum.ToString());
            }
            while (Directory.Exists(Path.Combine(archDirPath, lineName)));
            return lineName;
        }

        public static string GetNextArchElName(string lineDirPath, ArchiveMode archMode)
        {
            string[] dl = Directory.GetDirectories(lineDirPath, archMode == ArchiveMode.Diff ? "D_*" : "I_*");
            int maxNum = -1;
            foreach (string dir in dl)
            {
                int n;
                if (!int.TryParse(Path.GetFileName(dir).Substring(2).Trim(), out n)) continue;
                if (maxNum < n) maxNum = n;
            }
            string nextName = "";
            do
            {
                maxNum += 1;
                nextName = (archMode == ArchiveMode.Diff ? "D_" : "I_") + (maxNum < 100 ? maxNum.ToString("00") : maxNum.ToString());
            }
            while (Directory.Exists(Path.Combine(lineDirPath, nextName)));
            return nextName;
        }

        [System.Runtime.InteropServices.DllImport("kernel32.dll", SetLastError = true)]
        [return: System.Runtime.InteropServices.MarshalAs(System.Runtime.InteropServices.UnmanagedType.Bool)]
        static extern bool DeleteFile(string lpFileName);

        [System.Runtime.InteropServices.DllImport("kernel32.dll", SetLastError = true)]
        [return: System.Runtime.InteropServices.MarshalAs(System.Runtime.InteropServices.UnmanagedType.Bool)]
        static extern bool DeleteFileW([System.Runtime.InteropServices.MarshalAs(System.Runtime.InteropServices.UnmanagedType.LPWStr)]string lpFileName);

        /// <summary>Удаление каталога с файлами для чтения</summary>
        static void DeleteDir(string dirPath)
        {
            string[] fl = Directory.GetFiles(dirPath, "*", SearchOption.AllDirectories);
            foreach (string file in fl) File.SetAttributes(file, FileAttributes.Normal);

            try
            {
                Directory.Delete(dirPath, true);
            }
            catch (Exception ex)
            {
                if (ex.GetType() == typeof(IOException)) throw new Exception(string.Format("Не удалось удалить каталог '{0}', поскольку он заблокирован другим процессом", dirPath));
                else throw ex;
            }
        }

        #endregion *** Архивация *****************************************************************************************

        #region *** внутренние методы *********************************************************************************

        /// <summary>MD5 из файла </summary>
        /// <param name="fPath">путь к файлу</param>
        /// <param name="AddLen">добавить ли к md5 размер файла</param>
        static String GetMD5(String fPath)
        {
            byte[] buf = File.ReadAllBytes(fPath);
            byte[] hash = new MD5CryptoServiceProvider().ComputeHash(buf);
            return MD5String(hash);
        }

        // конвертирование в строку
        static string MD5String(byte[] hash)
        {
            StringBuilder sb = new StringBuilder(32);
            foreach (byte hex in hash)
                sb.Append(hex.ToString("X2"));
            return sb.ToString().ToUpper();
        }

        #endregion *** внутренние методы ******************************************************************************

    }
}
